/*! FixedHeader 3.1.7
 * ©2009-2020 SpryMedia Ltd - datatables.net/license
 */

/**
 * @summary     FixedHeader
 * @description Fix a table's header or footer, so it is always visible while
 *              scrolling
 * @version     3.1.7
 * @file        dataTables.fixedHeader.js
 * @author      SpryMedia Ltd (www.sprymedia.co.uk)
 * @contact     www.sprymedia.co.uk/contact
 * @copyright   Copyright 2009-2020 SpryMedia Ltd.
 *
 * This source file is free software, available under the following license:
 *   MIT license - http://datatables.net/license/mit
 *
 * This source file is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 *
 * For details please refer to: http://www.datatables.net
 */

(function (factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD
        define(['jquery', 'datatables.net'], function ($) {
            return factory($, window, document);
        });
    } else if (typeof exports === 'object') {
        // CommonJS
        module.exports = function (root, $) {
            if (!root) {
                root = window;
            }

            if (!$ || !$.fn.dataTable) {
                $ = require('datatables.net')(root, $).$;
            }

            return factory($, root, root.document);
        };
    } else {
        // Browser
        factory(jQuery, window, document);
    }
}(function ($, window, document, undefined) {
    'use strict';
    var DataTable = $.fn.dataTable;


    var _instCounter = 0;

    var FixedHeader = function (dt, config) {
        // Sanity check - you just know it will happen
        if (!(this instanceof FixedHeader)) {
            throw "FixedHeader must be initialised with the 'new' keyword.";
        }

        // Allow a boolean true for defaults
        if (config === true) {
            config = {};
        }

        dt = new DataTable.Api(dt);

        this.c = $.extend(true, {}, FixedHeader.defaults, config);

        this.s = {
            dt: dt,
            position: {
                theadTop: 0,
                tbodyTop: 0,
                tfootTop: 0,
                tfootBottom: 0,
                width: 0,
                left: 0,
                tfootHeight: 0,
                theadHeight: 0,
                windowHeight: $(window).height(),
                visible: true
            },
            headerMode: null,
            footerMode: null,
            autoWidth: dt.settings()[0].oFeatures.bAutoWidth,
            namespace: '.dtfc' + (_instCounter++),
            scrollLeft: {
                header: -1,
                footer: -1
            },
            enable: true
        };

        this.dom = {
            floatingHeader: null,
            thead: $(dt.table().header()),
            tbody: $(dt.table().body()),
            tfoot: $(dt.table().footer()),
            header: {
                host: null,
                floating: null,
                placeholder: null
            },
            footer: {
                host: null,
                floating: null,
                placeholder: null
            }
        };

        this.dom.header.host = this.dom.thead.parent();
        this.dom.footer.host = this.dom.tfoot.parent();

        var dtSettings = dt.settings()[0];
        if (dtSettings._fixedHeader) {
            throw "FixedHeader already initialised on table " + dtSettings.nTable.id;
        }

        dtSettings._fixedHeader = this;

        this._constructor();
    };


    /*
     * Variable: FixedHeader
     * Purpose:  Prototype for FixedHeader
     * Scope:    global
     */
    $.extend(FixedHeader.prototype, {
        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * API methods
         */

        /**
         * Kill off FH and any events
         */
        destroy: function () {
            this.s.dt.off('.dtfc');
            $(window).off(this.s.namespace);

            if (this.c.header) {
                this._modeChange('in-place', 'header', true);
            }

            if (this.c.footer && this.dom.tfoot.length) {
                this._modeChange('in-place', 'footer', true);
            }
        },

        /**
         * Enable / disable the fixed elements
         *
         * @param  {boolean} enable `true` to enable, `false` to disable
         */
        enable: function (enable, update) {
            this.s.enable = enable;

            if (update || update === undefined) {
                this._positions();
                this._scroll(true);
            }
        },

        /**
         * Get enabled status
         */
        enabled: function () {
            return this.s.enable;
        },

        /**
         * Set header offset
         *
         * @param  {int} new value for headerOffset
         */
        headerOffset: function (offset) {
            if (offset !== undefined) {
                this.c.headerOffset = offset;
                this.update();
            }

            return this.c.headerOffset;
        },

        /**
         * Set footer offset
         *
         * @param  {int} new value for footerOffset
         */
        footerOffset: function (offset) {
            if (offset !== undefined) {
                this.c.footerOffset = offset;
                this.update();
            }

            return this.c.footerOffset;
        },


        /**
         * Recalculate the position of the fixed elements and force them into place
         */
        update: function () {
            var table = this.s.dt.table().node();

            if ($(table).is(':visible')) {
                this.enable(true, false);
            } else {
                this.enable(false, false);
            }

            this._positions();
            this._scroll(true);
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Constructor
         */

        /**
         * FixedHeader constructor - adding the required event listeners and
         * simple initialisation
         *
         * @private
         */
        _constructor: function () {
            var that = this;
            var dt = this.s.dt;

            $(window)
                .on('scroll' + this.s.namespace, function () {
                    that._scroll();
                })
                .on('resize' + this.s.namespace, DataTable.util.throttle(function () {
                    that.s.position.windowHeight = $(window).height();
                    that.update();
                }, 50));

            var autoHeader = $('.fh-fixedHeader');
            if (!this.c.headerOffset && autoHeader.length) {
                this.c.headerOffset = autoHeader.outerHeight();
            }

            var autoFooter = $('.fh-fixedFooter');
            if (!this.c.footerOffset && autoFooter.length) {
                this.c.footerOffset = autoFooter.outerHeight();
            }

            dt.on('column-reorder.dt.dtfc column-visibility.dt.dtfc draw.dt.dtfc column-sizing.dt.dtfc responsive-display.dt.dtfc', function () {
                that.update();
            });

            dt.on('destroy.dtfc', function () {
                that.destroy();
            });

            this._positions();
            this._scroll();
        },


        /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
         * Private methods
         */

        /**
         * Clone a fixed item to act as a place holder for the original element
         * which is moved into a clone of the table element, and moved around the
         * document to give the fixed effect.
         *
         * @param  {string}  item  'header' or 'footer'
         * @param  {boolean} force Force the clone to happen, or allow automatic
         *   decision (reuse existing if available)
         * @private
         */
        _clone: function (item, force) {
            var dt = this.s.dt;
            var itemDom = this.dom[item];
            var itemElement = item === 'header' ?
                this.dom.thead :
                this.dom.tfoot;

            if (!force && itemDom.floating) {
                // existing floating element - reuse it
                itemDom.floating.removeClass('fixedHeader-floating fixedHeader-locked');
            } else {
                if (itemDom.floating) {
                    itemDom.placeholder.remove();
                    this._unsize(item);
                    itemDom.floating.children().detach();
                    itemDom.floating.remove();
                }

                itemDom.floating = $(dt.table().node().cloneNode(false))
                    .css('table-layout', 'fixed')
                    .attr('aria-hidden', 'true')
                    .removeAttr('id')
                    .append(itemElement)
                    .appendTo('body');

                // Insert a fake thead/tfoot into the DataTable to stop it jumping around
                itemDom.placeholder = itemElement.clone(false);
                itemDom.placeholder
                    .find('*[id]')
                    .removeAttr('id');

                itemDom.host.prepend(itemDom.placeholder);

                // Clone widths
                this._matchWidths(itemDom.placeholder, itemDom.floating);
            }
        },

        /**
         * Copy widths from the cells in one element to another. This is required
         * for the footer as the footer in the main table takes its sizes from the
         * header columns. That isn't present in the footer so to have it still
         * align correctly, the sizes need to be copied over. It is also required
         * for the header when auto width is not enabled
         *
         * @param  {jQuery} from Copy widths from
         * @param  {jQuery} to   Copy widths to
         * @private
         */
        _matchWidths: function (from, to) {
            var get = function (name) {
                return $(name, from)
                    .map(function () {
                        return $(this).width();
                    }).toArray();
            };

            var set = function (name, toWidths) {
                $(name, to).each(function (i) {
                    $(this).css({
                        width: toWidths[i],
                        minWidth: toWidths[i]
                    });
                });
            };

            var thWidths = get('th');
            var tdWidths = get('td');

            set('th', thWidths);
            set('td', tdWidths);
        },

        /**
         * Remove assigned widths from the cells in an element. This is required
         * when inserting the footer back into the main table so the size is defined
         * by the header columns and also when auto width is disabled in the
         * DataTable.
         *
         * @param  {string} item The `header` or `footer`
         * @private
         */
        _unsize: function (item) {
            var el = this.dom[item].floating;

            if (el && (item === 'footer' || (item === 'header' && !this.s.autoWidth))) {
                $('th, td', el).css({
                    width: '',
                    minWidth: ''
                });
            } else if (el && item === 'header') {
                $('th, td', el).css('min-width', '');
            }
        },

        /**
         * Reposition the floating elements to take account of horizontal page
         * scroll
         *
         * @param  {string} item       The `header` or `footer`
         * @param  {int}    scrollLeft Document scrollLeft
         * @private
         */
        _horizontal: function (item, scrollLeft) {
            var itemDom = this.dom[item];
            var position = this.s.position;
            var lastScrollLeft = this.s.scrollLeft;

            if (itemDom.floating && lastScrollLeft[item] !== scrollLeft) {
                itemDom.floating.css('left', position.left - scrollLeft);

                lastScrollLeft[item] = scrollLeft;
            }
        },

        /**
         * Change from one display mode to another. Each fixed item can be in one
         * of:
         *
         * * `in-place` - In the main DataTable
         * * `in` - Floating over the DataTable
         * * `below` - (Header only) Fixed to the bottom of the table body
         * * `above` - (Footer only) Fixed to the top of the table body
         *
         * @param  {string}  mode        Mode that the item should be shown in
         * @param  {string}  item        'header' or 'footer'
         * @param  {boolean} forceChange Force a redraw of the mode, even if already
         *     in that mode.
         * @private
         */
        _modeChange: function (mode, item, forceChange) {
            var dt = this.s.dt;
            var itemDom = this.dom[item];
            var position = this.s.position;

            // It isn't trivial to add a !important css attribute...
            var importantWidth = function (w) {
                itemDom.floating.attr('style', function (i, s) {
                    return (s || '') + 'width: ' + w + 'px !important;';
                });
            };

            // Record focus. Browser's will cause input elements to loose focus if
            // they are inserted else where in the doc
            var tablePart = this.dom[item === 'footer' ? 'tfoot' : 'thead'];
            var focus = $.contains(tablePart[0], document.activeElement) ?
                document.activeElement :
                null;

            if (focus) {
                focus.blur();
            }

            if (mode === 'in-place') {
                // Insert the header back into the table's real header
                if (itemDom.placeholder) {
                    itemDom.placeholder.remove();
                    itemDom.placeholder = null;
                }

                this._unsize(item);

                if (item === 'header') {
                    itemDom.host.prepend(tablePart);
                } else {
                    itemDom.host.append(tablePart);
                }

                if (itemDom.floating) {
                    itemDom.floating.remove();
                    itemDom.floating = null;
                }
            } else if (mode === 'in') {
                // Remove the header from the read header and insert into a fixed
                // positioned floating table clone
                this._clone(item, forceChange);

                itemDom.floating
                    .addClass('fixedHeader-floating')
                    .css(item === 'header' ? 'top' : 'bottom', this.c[item + 'Offset'])
                    .css('left', position.left + 'px');

                importantWidth(position.width);

                if (item === 'footer') {
                    itemDom.floating.css('top', '');
                }
            } else if (mode === 'below') { // only used for the header
                // Fix the position of the floating header at base of the table body
                this._clone(item, forceChange);

                itemDom.floating
                    .addClass('fixedHeader-locked')
                    .css('top', position.tfootTop - position.theadHeight)
                    .css('left', position.left + 'px');

                importantWidth(position.width);
            } else if (mode === 'above') { // only used for the footer
                // Fix the position of the floating footer at top of the table body
                this._clone(item, forceChange);

                itemDom.floating
                    .addClass('fixedHeader-locked')
                    .css('top', position.tbodyTop)
                    .css('left', position.left + 'px');

                importantWidth(position.width);
            }

            // Restore focus if it was lost
            if (focus && focus !== document.activeElement) {
                setTimeout(function () {
                    focus.focus();
                }, 10);
            }

            this.s.scrollLeft.header = -1;
            this.s.scrollLeft.footer = -1;
            this.s[item + 'Mode'] = mode;
        },

        /**
         * Cache the positional information that is required for the mode
         * calculations that FixedHeader performs.
         *
         * @private
         */
        _positions: function () {
            var dt = this.s.dt;
            var table = dt.table();
            var position = this.s.position;
            var dom = this.dom;
            var tableNode = $(table.node());

            // Need to use the header and footer that are in the main table,
            // regardless of if they are clones, since they hold the positions we
            // want to measure from
            var thead = tableNode.children('thead');
            var tfoot = tableNode.children('tfoot');
            var tbody = dom.tbody;

            position.visible = tableNode.is(':visible');
            position.width = tableNode.outerWidth();
            position.left = tableNode.offset().left;
            position.theadTop = thead.offset().top;
            position.tbodyTop = tbody.offset().top;
            position.tbodyHeight = tbody.outerHeight();
            position.theadHeight = position.tbodyTop - position.theadTop;

            if (tfoot.length) {
                position.tfootTop = tfoot.offset().top;
                position.tfootBottom = position.tfootTop + tfoot.outerHeight();
                position.tfootHeight = position.tfootBottom - position.tfootTop;
            } else {
                position.tfootTop = position.tbodyTop + tbody.outerHeight();
                position.tfootBottom = position.tfootTop;
                position.tfootHeight = position.tfootTop;
            }
        },


        /**
         * Mode calculation - determine what mode the fixed items should be placed
         * into.
         *
         * @param  {boolean} forceChange Force a redraw of the mode, even if already
         *     in that mode.
         * @private
         */
        _scroll: function (forceChange) {
            var windowTop = $(document).scrollTop();
            var windowLeft = $(document).scrollLeft();
            var position = this.s.position;
            var headerMode, footerMode;

            if (this.c.header) {
                if (!this.s.enable) {
                    headerMode = 'in-place';
                } else if (!position.visible || windowTop <= position.theadTop - this.c.headerOffset) {
                    headerMode = 'in-place';
                } else if (windowTop <= position.tfootTop - position.theadHeight - this.c.headerOffset) {
                    headerMode = 'in';
                } else {
                    headerMode = 'below';
                }

                if (forceChange || headerMode !== this.s.headerMode) {
                    this._modeChange(headerMode, 'header', forceChange);
                }

                this._horizontal('header', windowLeft);
            }

            if (this.c.footer && this.dom.tfoot.length) {
                if (!this.s.enable) {
                    footerMode = 'in-place';
                } else if (!position.visible || windowTop + position.windowHeight >= position.tfootBottom + this.c.footerOffset) {
                    footerMode = 'in-place';
                } else if (position.windowHeight + windowTop > position.tbodyTop + position.tfootHeight + this.c.footerOffset) {
                    footerMode = 'in';
                } else {
                    footerMode = 'above';
                }

                if (forceChange || footerMode !== this.s.footerMode) {
                    this._modeChange(footerMode, 'footer', forceChange);
                }

                this._horizontal('footer', windowLeft);
            }
        }
    });


    /**
     * Version
     * @type {String}
     * @static
     */
    FixedHeader.version = "3.1.7";

    /**
     * Defaults
     * @type {Object}
     * @static
     */
    FixedHeader.defaults = {
        header: true,
        footer: false,
        headerOffset: 0,
        footerOffset: 0
    };


    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * DataTables interfaces
     */

// Attach for constructor access
    $.fn.dataTable.FixedHeader = FixedHeader;
    $.fn.DataTable.FixedHeader = FixedHeader;


// DataTables creation - check if the FixedHeader option has been defined on the
// table and if so, initialise
    $(document).on('init.dt.dtfh', function (e, settings, json) {
        if (e.namespace !== 'dt') {
            return;
        }

        var init = settings.oInit.fixedHeader;
        var defaults = DataTable.defaults.fixedHeader;

        if ((init || defaults) && !settings._fixedHeader) {
            var opts = $.extend({}, defaults, init);

            if (init !== false) {
                new FixedHeader(settings, opts);
            }
        }
    });

// DataTables API methods
    DataTable.Api.register('fixedHeader()', function () {
    });

    DataTable.Api.register('fixedHeader.adjust()', function () {
        return this.iterator('table', function (ctx) {
            var fh = ctx._fixedHeader;

            if (fh) {
                fh.update();
            }
        });
    });

    DataTable.Api.register('fixedHeader.enable()', function (flag) {
        return this.iterator('table', function (ctx) {
            var fh = ctx._fixedHeader;

            flag = (flag !== undefined ? flag : true);
            if (fh && flag !== fh.enabled()) {
                fh.enable(flag);
            }
        });
    });

    DataTable.Api.register('fixedHeader.enabled()', function () {
        if (this.context.length) {
            var fh = this.content[0]._fixedHeader;

            if (fh) {
                return fh.enabled();
            }
        }

        return false;
    });

    DataTable.Api.register('fixedHeader.disable()', function () {
        return this.iterator('table', function (ctx) {
            var fh = ctx._fixedHeader;

            if (fh && fh.enabled()) {
                fh.enable(false);
            }
        });
    });

    $.each(['header', 'footer'], function (i, el) {
        DataTable.Api.register('fixedHeader.' + el + 'Offset()', function (offset) {
            var ctx = this.context;

            if (offset === undefined) {
                return ctx.length && ctx[0]._fixedHeader ?
                    ctx[0]._fixedHeader[el + 'Offset']() :
                    undefined;
            }

            return this.iterator('table', function (ctx) {
                var fh = ctx._fixedHeader;

                if (fh) {
                    fh[el + 'Offset'](offset);
                }
            });
        });
    });


    return FixedHeader;
}));
